using System;
using System.IO;

namespace Stetson.Emulator.Hardware {
	#region Extension Methods

	static class Extensions {

		public static bool HasData(this IOPort.CommandID id) {
			switch (id) {
				case IOPort.CommandID.VariableHeader:
				case IOPort.CommandID.Data:
				case IOPort.CommandID.Skip:
				case IOPort.CommandID.DeleteVariable:
				case IOPort.CommandID.RequestVariable:
				case IOPort.CommandID.RequestToSendVariable:
					return true;
				default:
					return false;
			}
		}
	}

	#endregion

	public class IOPort {



		private bool internalD0 = true;
		/// <summary>
		/// Gets or sets the level of the least significant data line.
		/// </summary>
		public bool InternalD0 {
			get { return this.internalD0; }
			set { this.internalD0 = value; this.UpdateLinkInterrupt(); }
		}

		private bool internalD1 = true;
		/// <summary>
		/// Gets or sets the level of the most significant data line.
		/// </summary>
		public bool InternalD1 {
			get { return this.internalD1; }
			set { this.internalD1 = value; this.UpdateLinkInterrupt(); }
		}

		private bool externalD0 = true;
		public bool ExternalD0 {
			get { return this.externalD0; }
			set { this.externalD0 = value; this.UpdateLinkInterrupt(); }
		}


		private bool externalD1 = true;
		public bool ExternalD1 {
			get { return this.externalD1; }
			set { this.externalD1 = value; this.UpdateLinkInterrupt(); }
		}

		private bool OldOverallLevel = true;
		private void UpdateLinkInterrupt() {
			bool NewOverallLevel = this.CurrentD0 && this.CurrentD1;
			if (this.OldOverallLevel && !NewOverallLevel && this.Calculator.InterruptMaskLinkPort) {
				this.Calculator.InterruptGeneratedByLinkPort = true;
			}
			this.OldOverallLevel = NewOverallLevel;
		}

		public bool CurrentD0 {
			get { return this.internalD0 && this.externalD0; }
		}
		public bool CurrentD1 {
			get { return this.internalD1 && this.externalD1; }
		}

		private bool linkAssistEnabled;
		public bool LinkAssistEnabled {
			get { return this.linkAssistEnabled; }
			set { 
				this.linkAssistEnabled = value;
				if (this.linkAssistEnabled) {
					throw new Exception();
				}
			}
		}

		private readonly TI83Plus calculator;
		public TI83Plus Calculator {
			get { return this.calculator; }
		}

		public IOPort(TI83Plus calculator) {
			this.calculator = calculator;
		}


		#region Public Methods

		/// <summary>
		/// Reads the status of the port.
		/// </summary>
		public byte ReadStatus() {
			return (byte)(
				(this.CurrentD0 ? 0x01 : 0x00) |
				(this.CurrentD1 ? 0x02 : 0x00) |
				(this.LinkAssistEnabled ? 0x04 : 0x00) |
				(this.InternalD0 ? 0x00 : 0x10) |
				(this.InternalD1 ? 0x00 : 0x20)
			);
		}

		/// <summary>
		/// Writes directly to the port status.
		/// </summary>
		public void WriteStatus(byte data) {
			//Console.WriteLine("WRITE: {0:X2}", data);
			this.LinkAssistEnabled = (data & 0x04) != 0;
			this.internalD0 = (data & 0x01) == 0;
			this.InternalD1 = (data & 0x02) == 0;
		}

        public void WriteExternalStatus(byte data)
        {
            this.ExternalD0 = (data & 0x01) == 0;
            this.ExternalD1 = (data & 0x02) == 0;
        }

		/// <summary>
		/// Write a value out to the port using the Texas Instruments link protocol.
		/// </summary>
		/// <param name="data">The data to write.</param>
		public void WriteData(byte data) {
			Console.WriteLine("-> {0:X2}", data);
			for (int i = 0; i < 8; ++i) {
				this.WriteData((data & (1 << i)) != 0);
			}
		}
		/// <summary>
		/// Read a value from the port using the Texas Instruments link protocol.
		/// </summary>
		public byte ReadData() {
			byte Result = 0;
			for (int i = 0; i < 8; ++i) {
				Result >>= 1;
				if (this.ReadDataBit()) Result |= 0x80;
			}
			Console.WriteLine("<- {0:X2}", Result);
			return Result;
		}

		#endregion


		#region Private Methods

		/// <summary>
		/// Waits for the link lines to reach a particular level.
		/// </summary>
		/// <param name="d0">The desired level for the least significant data line.</param>
		/// <param name="d1">The desired level for the most significant data line.</param>
		private void WaitStatus(bool d0, bool d1) {
			for (int i = 0; i < 6000000; ++i) {
				if (this.CurrentD0 == d0 && this.CurrentD1 == d1) return;
				this.Calculator.FetchExecute(4);
			}
			throw new TimeoutException();
		}

		/// <summary>
		/// Write a bit of data using the TI link protocol.
		/// </summary>
		/// <param name="bit">The bit to write.</param>
		private void WriteData(bool bit) {
			try {
				if (bit) {
					this.ExternalD1 = false;
					this.WaitStatus(false, false);
					this.ExternalD1 = true;
				} else {
					this.ExternalD0 = false;
					this.WaitStatus(false, false);
					this.ExternalD0 = true;
				}
				this.WaitStatus(true, true);
			} finally {
				this.ExternalD0 = true;
				this.ExternalD1 = true;
			}
		}

		/// <summary>
		/// Reads a bit of data using the Texas Instruments link protocol.
		/// </summary>
		private bool ReadDataBit() {

			bool Result = false;

			try {

				for (int i = 0; i < 6000000; ++i) {
					if (!this.CurrentD0) {
						Result = false;
						this.ExternalD1 = false;
						goto GotBit;
					}
					if (!this.CurrentD1) {
						Result = true;
						this.ExternalD0 = false;
						goto GotBit;
					}

					this.Calculator.FetchExecute(4);
				}

				throw new TimeoutException();

			GotBit: ;

				this.WaitStatus(!Result, Result);
				this.ExternalD0 = true;
				this.ExternalD1 = true;
				this.WaitStatus(true, true);
				return Result;
			} finally {
				this.ExternalD0 = true;
				this.ExternalD1 = true;
			}
		}

		#endregion


		#region High-Level Protocol

		/// <summary>
		/// Represents a machine ID.
		/// </summary>
		public enum MachineID : byte {
			ComputerToTI82 = 0x02,
			ComputerToTI83 = 0x03,
			ComputerToTI83Plus = 0x23,
			TI83Plus = 0x73,
			TI82 = 0x82,
			TI83 = 0x83,
		}

		/// <summary>
		/// Represents a command ID.
		/// </summary>
		public enum CommandID : byte {
			VariableHeader = 0x06,
			ClearToSend = 0x09,
			Data = 0x15,
			RequestVersion = 0x2D,
			Skip = 0x36,
			Acknowledge = 0x56,
			ChecksumError = 0x5A,
			CheckReady = 0x68,
			RequestScreenshot = 0x6D,
			DeleteVariable = 0x88,
			EndOfTransmittion = 0x92,
			RequestVariable = 0xA2,
			RequestToSendVariable = 0xC9,
		}

		/// <summary>
		/// Represents a rejection code.
		/// </summary>
		public enum RejectionCode {
			/// <summary>
			/// The entire transmission has been cancelled.
			/// </summary>
			Exit = 0x01,
			/// <summary>
			/// The current variable has been skipped.
			/// </summary>
			Skip = 0x02,
			/// <summary>
			/// The receiving calculator is out of memory.
			/// </summary>
			OutOfMemory = 0x03,
		}

		/// <summary>
		/// Create a packet from some data.
		/// </summary>
		/// <param name="machine">The machine sending the packet.</param>
		/// <param name="command">The packet command type.</param>
		/// <param name="data">The data to send.</param>
		/// <returns>A formatted packet suitable for transmission.</returns>
		public static byte[] CreatePacket(MachineID machine, CommandID command, byte[] data) {

			if (data == null) data = new byte[0];

			byte[] Result = new byte[4 + data.Length + (data.Length != 0 ? 2 : 0)];
			Result[0] = (byte)machine;
			Result[1] = (byte)command;
			Result[2] = (byte)data.Length;
			Result[3] = (byte)(data.Length >> 8);

			if (data.Length > 0) {
				ushort Checksum = 0;
				for (int i = 0; i < data.Length; ++i) {
					Checksum += data[i];
					Result[i + 4] = data[i];
				}
				Result[Result.Length - 2] = (byte)Checksum;
				Result[Result.Length - 1] = (byte)(Checksum >> 8);
			}

			return Result;
		}

		/// <summary>
		/// Write a packet to the calculator.
		/// </summary>
		/// <param name="machine">The machine sending the packet.</param>
		/// <param name="command">The packet command type.</param>
		/// <param name="data">The data to send.</param>
		public void WritePacket(MachineID machine, CommandID command, byte[] data) {
			foreach (byte Data in CreatePacket(machine, command, data)) {
				this.WriteData(Data);
			}
		}

		/// <summary>
		/// Reads a packet from the calculator.
		/// </summary>
		/// <param name="machine">The machine sending the packet.</param>
		/// <param name="command">The packet command type.</param>
		/// <returns>The data accompanying the packet.</returns>
		public byte[] ReadPacket(out MachineID machine, out CommandID command) {
			machine = (MachineID)this.ReadData();
			command = (CommandID)this.ReadData();

			ushort DataLength = this.ReadData();
			DataLength += (ushort)(this.ReadData() * 256);

			if (!command.HasData()) DataLength = 0;

			if (DataLength == 0) {
				return null;
			} else {
				byte[] Data = new byte[DataLength];
				ushort Checksum = 0;
				for (int i = 0; i < DataLength; ++i) {
					Data[i] = this.ReadData();
					Checksum += Data[i];
				}
				ushort ReferenceChecksum = this.ReadData();
				ReferenceChecksum += (ushort)(this.ReadData() * 256);
				Console.WriteLine(Checksum == ReferenceChecksum);
				return Data;
			}
		}

		/// <summary>
		/// Reads a packet of an expected type.
		/// </summary>
		/// <param name="command">The expected format.</param>
		/// <returns>The data accompanying the packet.</returns>
		public byte[] ReadExpectedPacket(CommandID command) {
			MachineID Machine;
			CommandID Command;
			byte[] Result = this.ReadPacket(out Machine, out Command);
			if (Command != command) throw new InvalidDataException(string.Format("Received {0} (Expected {1}).", Command, command));
			return Result;

		}

		#endregion
	}
}
